/*	
	stencil - action script 3 isometric game engine.

	http://code.google.com/p/stencilas3/
	
	Copyright (c) 2010 Max Zhuravkov, All Rights Reserved.

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/
package stencil.core.engine 
{
	import flash.utils.Dictionary;
	import stencil.core.errors.ArgumentIsNullError;
	import stencil.core.engine.utils.getClassName;
	
	/**
	 * A base implemenation of IObjectFactory interface.
	 * 
	 * @author Max Zhuravkov
	 */
	public class ObjectFactory implements IObjectFactory {
		
		private var _objectCounter:Object = { };
		
		private var _genericType:Class;
		
		private var _objectTypes:Dictionary = new Dictionary();
		
		private var _objectProperties:Dictionary = new Dictionary();
		
		private var _componentTypes:Dictionary = new Dictionary();
		
		private var _componentProperties:Dictionary = new Dictionary();
		
		private var _components:Dictionary = new Dictionary();
		
		//------------------------------------------------------------------------------------------------
		// Constructor
		//------------------------------------------------------------------------------------------------
		
		public function ObjectFactory(genericType:Class) {
			this._genericType = genericType;
		}
		
		/**
		 * @inheritDoc
		 */
		public function get genericType():Class {
			return this._genericType;
		}
		
		//------------------------------------------------------------------------------------------------
		// Public methods
		//------------------------------------------------------------------------------------------------
		
		/**
		 * @inheritDoc
		 */
		public final function addComponentType(type:String, componentClass:Class, properties:Object = null):void {
			this.registerType("Component type", "componentClass", type, componentClass, this._componentTypes);
			
			this._componentProperties[type] = properties;
		}
		
		/**
		 * @inheritDoc
		 */
		public final function removeComponentType(type:String):void {
			this.unregisterType("Component type", type, this._componentTypes);
		}
		
		/**
		 * @inheritDoc
		 */
		public final function addObjectType(type:String, objectClass:Class, properties:Object = null, components:Object = null):void {
			this.registerType("Object type", "objectClass", type, objectClass, this._objectTypes);
			
			this._components[type] = components;
			this._objectProperties[type] = properties;
		}
		
		/**
		 * @inheritDoc
		 */
		public function removeObjectType(type:String):void {
			this.unregisterType("Object type", type, this._objectTypes);
			
			delete this._components[type];
		}
		
		/**
		 * @inheritDoc
		 */
		public function createObject(type:String, owner:ISceneObject = null, id:String = null):Object {
			if (!type) {
				throw new ArgumentIsNullError("type");
			}
			var clazz:Class = this._objectTypes[type];
			if (!clazz) {
				throw new ArgumentError("Object type '" + type + "' is not registered.");
			}
			
			var components:Object = this._components[type];
			var properties:Object = this._objectProperties[type];
			var o:Object = this.newObjectInstance(type, clazz, this.genericType);
			
			if (o is IIdentifiable) {
				if (!id) {
					id = this.generateObjectIdentifier(o, type, this._objectCounter);
				}
				(o as IIdentifiable).id = id;
			}
			
			if (owner && o is IComponent) {
				(o as IComponent).owner = owner;
			}
			
			if(properties){
				this.setProperties(type, o, properties);
			}
			
			if (components) {
				this.attachComponents(o, components);
			}
			
			return this.postConstruct(o);
		}
		
		/**
		 * @inheritDoc
		 */
		public function releaseObject(o:Object):void{
			
		}
		
		//------------------------------------------------------------------------------------------------
		// Private methods
		//------------------------------------------------------------------------------------------------
		
		private function registerType(categoryName:String, classAttributeName:String, type:String, clazz:Class, container:Dictionary):void {
			if (!type) {
				throw new ArgumentIsNullError("type");
			}
			if (!clazz) {
				throw new ArgumentIsNullError(classAttributeName);
			}
			var anotherClazz:Class = container[type];
			if (anotherClazz){
				throw new ArgumentError(categoryName + " '" + type + "' has been already registered for " + getClassName(anotherClazz) + "'." );
			}
			container[type] = clazz;
		}
		
		private function unregisterType(categoryName:String, type:String, container:Dictionary):void {
			if (!type) {
				throw new ArgumentIsNullError("type");
			}
			if (!container[type]) {
				throw new ArgumentError(categoryName + " '" + type + "' is not registered.");
			}
			delete container[type];
		}
		
		//------------------------------------------------------------------------------------------------
		// Protected properties
		//------------------------------------------------------------------------------------------------
		
		protected final function get componentTypes():Dictionary {
			return this._componentTypes;
		}
		
		//------------------------------------------------------------------------------------------------
		// Protected methods
		//------------------------------------------------------------------------------------------------
		
		/**
		 * Creates an instance of class specified by `clazz` argument that is subtype of the 'requiredType' parameter type.
		 * 
		 * @param	type	the type alias.
		 * @param	clazz	the class.
		 * @param	requiredType	the type which represents subtype limitation (the class specified by 'clazz' attribute should be a subclass of this type).
		 * 
		 * @return	an instance of 'clazz' class.
		 */
		protected function newObjectInstance(type:String, clazz:Class, requiredType:Class):Object {
			var o:Object = new clazz();
			if (!(o is requiredType)) {
				throw new Error("Class '" + getClassName(o) + "' does not conform to '" + getClassName(requiredType) + "'." );
			}
			
			return o;
		}
		
		/**
		 * Called during instantiation process of the specified object in order to add the given components to the object.
		 * 
		 * @param	o	the object.
		 * @param	components	the components. 
		 */
		protected function attachComponents(o:Object, components:Object):void {
			if (o is IComponentList) {
				var componentList:IComponentList = o as IComponentList;
				for (var componentId:String in components) {
					var componentObject:Object = components[componentId];
					
					this.addComponentTo(componentList, componentId, componentObject);
				}
			}
		}
		
		/**
		 * Adds component type ot the specified object.
		 */
		protected function addComponentTo(target:IComponentList, componentId:String, componentObject:Object):void {
			var componentClass:Class = null;
			var properties:Object = null;
			
			if (componentObject is String) {
				componentClass = this.componentTypes[componentObject as String];
				properties = this._componentProperties[componentObject as String];
			}else {
				if (!(componentObject is Class)) {
					throw new Error("Unable to create an object of type '" + getClassName(target) +"'. Component descriptor is neither component class nor component type alias." );
				}
				componentClass = componentObject as Class;
			}
			
			var component:IComponent = this.newObjectInstance(null, componentClass, IComponent) as IComponent;
			if(properties){
				this.setProperties(null, component, properties);
			}
			target.addComponent(componentId, component);
		}
		
		
		
		/**
		 * Called during instantiation process of the specified object in order to set some of its properties.
		 * 
		 * @param	type 	the type alias.
		 * @param	target	the object.
		 * @param	properties	the object that contains property values to be set.
		 */
		protected function setProperties(type:String, target:Object, properties:Object):void {
			for (var property:String in properties) {
				var path:Array = property.split(".")
				var val:Object = properties[property];
				if (path.length == 1) {
					target[property] = this.evaluatePropertyValue(type, target, properties[property]);
				}else {
					var o:Object = target;
					while (o && path.length > 1) {
						var p:String = path.shift();
						o = o[p];
						if (o && path.length == 1) {
							o[path[0]] = this.evaluatePropertyValue(type, target, val);
						}
					}
				}
			}
		}
		
		protected function evaluatePropertyValue(type:String, target:Object, val:Object):Object {
			if (val is Function) {
				var f:Function = val as Function;
				return f.call(null, target, type);
			}
			return val;
		}
		
		/**
		 * Called when the specified object is completely created (all properties and components were set if any).
		 * 
		 * @param	o	the object.
		 */
		protected function postConstruct(o:Object):Object {
			return o;
		}
		
		/**
		 * Generates object identifier.
		 */
		protected function generateObjectIdentifier(o:Object, type:String, objectCounter:Object):String {
			var o:Object = objectCounter[type];
			var index:int = 0;
			if (o is int) {
				index = int(o);
			}
			var id:String = type + "_" + index;
			objectCounter[type] = ++ index;
			return id;
		}
	}

}